package first.group.ssh.version;

import first.group.ssh.ConnectionCallback;
import first.group.ssh.ReadBufferConnectStage;
import first.group.ssh.readBuffer.ReadBuffer;
import first.group.ssh.writebuffer.ByteArrayContent;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.logging.Level;
import java.util.logging.Logger;

public class VersionExchanger extends ReadBufferConnectStage {
    private static final Logger logger = Logger.getLogger("VersionFactory");

    private final ConnectionCallback callback;
    private ByteBuffer readBuffer = ByteBuffer.allocate(256);
    private String protoVersion = null, softwareVersion = null, comment = null;
    private Version serverVersion;
    private final Version clientVersion;

    public VersionExchanger(ConnectionCallback callback, Version clientVersion) {
        this.callback = callback;
        this.clientVersion = clientVersion;
    }

    @Override
    public boolean shouldRead() {
        return true;
    }
    
    public Version getClientVersion() {
        return clientVersion;
    }
    
    public Version getServerVersion() {
        return serverVersion;
    }

    private byte[] head = new byte[4];
    private boolean processHeader() {
        readBuffer.get(head);
        if (head[0] != 'S') {
            return false;
        }
        if (head[1] != 'S') {
            return false;
        }
        if (head[2] != 'H') {
            return false;
        }
        return head[3] == '-';
    }

    private boolean processContent() throws IOException {
        if (readBuffer.limit() < 9) {
            return false;
        }
        
        if (!processHeader()) {
            throw new IOException("Invalid header for SSH");
        }

        var builder = new StringBuilder();
        while (true) {
            if (!readBuffer.hasRemaining()) {
                return false;
            }
            var byteData = readBuffer.get();
            if (byteData <= 0) {
                throw new IOException("String is not ASCII");
            }
            if (byteData == '-') {
                break;
            }
            builder.append((char) byteData);
        }
        protoVersion = builder.toString();

        builder = new StringBuilder();
        boolean getCr = false;
        while (true) {
            if (!readBuffer.hasRemaining()) {
                return false;
            }
            var byteData = readBuffer.get();
            if (byteData <= 0) {
                throw new IOException("String is not ASCII");
            }
            if (byteData == '\r') {
                getCr = true;
                continue;
            }
            if (getCr) {
                if (byteData == '\n') {
                    softwareVersion = builder.toString();
                    break;
                } else {
                    throw new IOException("CR is not followed by LF");
                }
            }
            if (byteData == ' ') {
                softwareVersion = builder.toString();
                break;
            }
            builder.append((char) byteData);
        }

        if (getCr) {
            return true;
        }

        builder = new StringBuilder();
        while (true) {
            if (!readBuffer.hasRemaining()) {
                return false;
            }
            var byteData = readBuffer.get();
            if (byteData <= 0) {
                throw new IOException("String is not ASCII");
            }
            if (byteData == '\r') {
                getCr = true;
                continue;
            }
            if (getCr) {
                if (byteData == '\n') {
                    comment = builder.toString();
                    break;
                } else {
                    throw new IOException("CR is not followed by LF");
                }
            }
            builder.append((char) byteData);
        }

        return true;
    }

    private boolean process() throws IOException {
        if (readBuffer.position() < 9) {
            return false;
        }
        
        readBuffer.mark();
        readBuffer.limit(readBuffer.position());
        readBuffer.position(0);
        
        var read = processContent();
        if (read) {
            serverVersion = new Version(protoVersion, softwareVersion, comment);
            logger.log(Level.FINE, "Server version read: {0}", serverVersion);
            callback.finish();
            readBuffer = null;
            head = null;
            return true;
        } else {
            readBuffer.reset();
            readBuffer.limit(readBuffer.capacity());
        }
        return false;
    }
    
    @Override
    public void init() {
        logger.log(Level.FINE, "Client version write: {0}", clientVersion);
        callback.getWriteBuffer().append(new ByteArrayContent(clientVersion.toString() + "\r\n"));
    }

    @Override
    public void read(ReadBuffer buffer) throws IOException {
        buffer.read(readBuffer);
        
        buffer.mark();
        if (process()) {
            buffer.consume();
        } else {
            buffer.rewind();
        }
    }
}
